package mobilefeel.sdk;

import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serializable;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.Queue;

import mobilefeel.dto.AccelerometerRate;
import mobilefeel.dto.CustomData;
import mobilefeel.dto.DataTransferObject;
import mobilefeel.dto.DeviceConnect;
import mobilefeel.dto.ViewEvent;

import android.os.Build;
import android.util.Log;

final class NetworkController implements Runnable {
	
	private volatile Socket socket;
	private volatile ObjectOutputStream output;
	private volatile ObjectInputStream input;
	private MobileFeelService service;
	private volatile Queue<DataTransferObject> dataToSend;
	
	private Object lock = new Object();	
	private Thread currentThread;
	private boolean streamClosing = false;
	private boolean streamCanClose = false;
	
	private String address;
	private int port;
	private int timeout;
	
	NetworkController(MobileFeelService mobileFeelService) {
		this.service = mobileFeelService;
		this.dataToSend = new LinkedList<DataTransferObject>();
	}

	void close(){
		this.service.disconnect();
		
		synchronized(lock) {
			if (output != null && input != null && socket != null) {
				try {
					output.flush();
					output.close();
					input.close();
					socket.close();
				} catch (IOException e) {
					Log.e("MobileFeel", "IOException: " + e.getMessage());
				}
			}
		}
		Log.d("MobileFeel", "Connection closed");
	}
	
	void sendObject(DataTransferObject dto) {
		if (!streamClosing && socket != null && output != null && socket.isConnected() && !socket.isClosed()){
			synchronized(lock) {
				dataToSend.add(dto);
				lock.notifyAll();
			}
		} else
			streamCanClose = true;
	}
	
	void connect(String address, int port, int timeout) {
		this.address = address;
		this.port = port;
		this.timeout = timeout;
		
		Log.d("MobileFeel", "Connecting...");
		if(this.socket == null || this.socket.isClosed()){
			Thread thread = new Thread(this);
			currentThread = thread;
			thread.start();
		}
	}
	
	void disconnect() {
		if (socket != null  && socket.isConnected() && !socket.isClosed()) {
			Log.d("MobileFeel", "Disconnecting...");
			DataTransferObject dto = new DataTransferObject();
			dto.setDeviceId(this.service.getDeviceId());
			dto.setType(DataTransferObject.DEVICE_DISCONNECT);
			sendObject(dto);
		}
			
		synchronized(lock) {
			streamClosing = true;
			lock.notifyAll();
		}
	}

	@Override
	public void run() {
		if((this.socket == null || this.socket.isClosed()) && (Thread.currentThread() == currentThread)){
			synchronized (lock) {
				try {
					this.socket = new Socket();
					this.socket.connect(new InetSocketAddress(this.address, this.port), this.timeout);
					this.output = new ObjectOutputStream(this.socket.getOutputStream());
					this.output.flush();
					this.input = new ObjectInputStream(this.socket.getInputStream());
					streamClosing = false;
					streamCanClose = false;
					
					Runnable sendRunnable = new Runnable() {
						public void run() {
							while (!streamClosing || !dataToSend.isEmpty()) {
								synchronized(lock) {
									if (dataToSend.peek() != null) {
										try {
											output.writeObject(dataToSend.poll());
										} catch (IOException e) {
											Log.e("MobileFeel", "IOException: " + e.getMessage());
											streamCanClose = true;
										}
									} else {
										try {
											lock.wait();
										} catch (InterruptedException e) {}
									}
								}
							}
						}			
					};
					new Thread(sendRunnable).start();
				
					DeviceConnect deviceConnect = new DeviceConnect();
					deviceConnect.setDeviceId(this.service.getDeviceId());
					deviceConnect.setType(DataTransferObject.DEVICE_CONNECT);
					deviceConnect.setBrand(Build.BRAND);
					deviceConnect.setDevice(Build.DEVICE);
					deviceConnect.setDisplay(Build.DISPLAY);
					deviceConnect.setManufacturer(Build.MANUFACTURER);
					deviceConnect.setModel(Build.MODEL);
					deviceConnect.setProduct(Build.PRODUCT);
					sendObject(deviceConnect);
					
					service.connectionEstablished();
					Log.d("MobileFeel", "Connected");
				} catch (SocketTimeoutException e) {
					Log.d("MobileFeel", "Connection timed out");
					this.service.connectionFailed(e.getMessage());
					socketClose();
					return;
				} catch (ConnectException e) {
					Log.d("MobileFeel", "Connection refused");
					this.service.connectionFailed(e.getMessage());
					socketClose();
					return;
				} catch (UnknownHostException e) {
					Log.d("MobileFeel", "Unknown host");
					this.service.connectionFailed(e.getMessage());
					socketClose();
					return;				
				} catch (IOException e) {
					Log.d("MobileFeel", "Connection failed");
					this.service.connectionFailed(e.getMessage());
					socketClose();
					return;
				}				
			} 
			
			try {
				DataTransferObject dto = (DataTransferObject)this.input.readObject();
				while (true) {
					this.processObject(dto);
					if (!streamClosing && socket.isConnected() && !socket.isClosed()){
						dto = (DataTransferObject)this.input.readObject();
					}
					else {
						streamCanClose = true;
						break;
					}	
				}
			} catch (SocketException e) {
				Log.e("MobileFeel", "SocketException: " + e.getMessage());
				synchronized(lock) {
					streamClosing = true;
					lock.notifyAll();
				}
			} catch (ClassNotFoundException e) {
				Log.e("MobileFeel", "ClassNotFoundException: " + e.getMessage());
				return;
			} catch (ClassCastException e) { 
				Log.e("MobileFeel", "ClassCastException: " + e.getMessage());
				return;
			} catch (EOFException e) {
				streamCanClose = true;
			} catch (OptionalDataException e) {
				Log.e("MobileFeel", "OptionalDataException: " + e.getMessage());
				return;
			} catch (IOException e) {
				Log.e("MobileFeel", "IOException: " + e.getMessage());
				return;
			} finally {
				while (!streamClosing) {}
				while (!streamCanClose) {}
				close();
			}
		}
	}
	
	private void processObject(DataTransferObject dto) {
		switch (dto.getType()) {
			case DataTransferObject.DEVICE_DISCONNECT:
				synchronized(lock) {
					streamClosing = true;
					lock.notifyAll();
				}
				this.service.receiveDisconnect();
				break;
		
			case DataTransferObject.DEVICE_CUSTOM_DATA:
				CustomData customData = (CustomData) dto;
				this.service.receiveCustomData(customData.getText(), customData.getObject());
				break;
		
			case DataTransferObject.ACCELEROMETER_REGISTER_EVENT:
				this.service.registerAccelerometerEvent();
				break;	
				
			case DataTransferObject.ACCELEROMETER_UNREGISTER_EVENT:
				this.service.unregisterAccelerometerEvent();
				break;	

			case DataTransferObject.ACCELEROMETER_RATE_SET:
				AccelerometerRate rate = (AccelerometerRate) dto;
				this.service.setAccelerometerRate(rate.getRate());
				break;	

			case DataTransferObject.TOUCHSCREEN_REGISTER_CLICK_EVENT:
				ViewEvent viewEventClickR = (ViewEvent) dto;
				this.service.registerOnClickEvent(viewEventClickR.getId());
				break;	
				
			case DataTransferObject.TOUCHSCREEN_UNREGISTER_CLICK_EVENT:
				ViewEvent viewEventClickU = (ViewEvent) dto;
				this.service.unregisterOnClickEvent(viewEventClickU.getId());
				break;	
				
			case DataTransferObject.TOUCHSCREEN_REGISTER_LONGCLICK_EVENT:
				ViewEvent viewEventLongClickR = (ViewEvent) dto;
				this.service.registerOnLongClickEvent(viewEventLongClickR.getId());
				break;	
				
			case DataTransferObject.TOUCHSCREEN_UNREGISTER_LONGCLICK_EVENT:
				ViewEvent viewEventLongClickU = (ViewEvent) dto;
				this.service.unregisterOnLongClickEvent(viewEventLongClickU.getId());
				break;	
				
			case DataTransferObject.TOUCHSCREEN_REGISTER_TOUCH_EVENT:
				ViewEvent viewEventTouchR = (ViewEvent) dto;
				this.service.registerOnTouchEvent(viewEventTouchR.getId());
				break;	
				
			case DataTransferObject.TOUCHSCREEN_UNREGISTER_TOUCH_EVENT:
				ViewEvent viewEventTouchU = (ViewEvent) dto;
				this.service.unregisterOnTouchEvent(viewEventTouchU.getId());
				break;			
		}
	}
	
	private void socketClose() {
		try {
			socket.close();
		} catch (IOException e) {
			Log.e("MobileFeel", "IOException: " + e.getMessage());
		}
	}

	void sendCustomData(String text, Serializable object) {
		CustomData customData = new CustomData();
		customData.setType(DataTransferObject.DEVICE_CUSTOM_DATA);
		customData.setDeviceId(this.service.getDeviceId());
		customData.setText(text);
		customData.setObject(object);
		
		sendObject(customData);
	}

}